const globalKeys = Object.keys(globalThis)

export function stringifyTyped(data: any): string {
  if (data === null) return 'null'
  if (data === undefined) return 'undefined'
  if (data !== data) return 'typeof NaN'
  if (typeof data === 'symbol') return ''
  if (typeof data === 'boolean') return 'boolean'
  if (typeof data === 'bigint') return 'bigint'
  if (typeof data !== 'object') return JSON.stringify(data)
  if (Array.isArray(data)) return `[${data.map((_) => stringifyTyped(_)).join(', ')}]`
  if (data instanceof Set) {
    const values = new Set()
    for (const val of data) {
      values.add(stringifyTyped(val))
    }
    return `Set<${[...values].join(' | ')}>`
  }
  if (data instanceof Map) {
    const keys = new Set()
    const values = new Set()
    for (const [key, val] of data) {
      keys.add(stringifyTyped(key))
      values.add(stringifyTyped(val))
    }
    return `Map<${[...keys].join(' | ')}, ${[...values].join(' | ')}>`
  }
  if (data instanceof WeakSet) return `WeakSet<any>`
  if (data instanceof WeakMap) return `WeakMap<any, any>`
  if (data.constructor !== Object && globalKeys.includes(data.constructor.name)) {
    return data.constructor.name
  }
  return `{
      ${Object.entries(data)
        .map(([key, value]) => {
          value = stringifyTyped(value)
          if (!value) return ''
          return `${key}: ${value};`
        })
        .filter((str) => str)
        .join('\n')}
    }`
}

const nativeFuncMap = new Map<Function, string>(
  Object.entries({
    Object,
    Array,
    String,
    Boolean,
    Function,
    Symbol,
    console,
    Reflect,
    Date,
    Number,
    Math,
    parseInt,
    parseFloat,
    Error,
    Set,
    Map,
    WeakMap,
    WeakSet,
    Buffer,
    Int8Array,
    Int16Array,
    Int32Array,
    Uint8Array,
    Uint8ClampedArray,
    Uint16Array,
    Uint32Array,
    BigInt64Array,
    BigUint64Array,
    Float32Array,
    Float64Array,
  }).reduce((array, [key, value]) => {
    if (typeof value === 'function') {
      array.push([value, key])
      if (value.prototype && value !== Symbol) {
        const _prototype = value.prototype
        Reflect.ownKeys(_prototype).forEach((key$1) => {
          try {
            if (typeof key$1 === 'string') {
              const func = (_prototype as any)[key$1]
              if (typeof func === 'function') {
                array.push([func, `${key}.prototype.${key$1}`])
              }
            }
          } catch {}
        })
      }
    }
    Reflect.ownKeys(value).forEach((key$1) => {
      try {
        if (key$1 === 'prototype') return
        if (typeof key$1 === 'string') {
          const func = (value as any)[key$1]
          if (typeof func === 'function') {
            array.push([func, `${key}.${key$1}`])
          }
        }
      } catch {}
    })
    return array
  }, [] as [Function, string][])
)

export function stringifyValue(data: any): string {
  if (data === undefined) return 'undefined'
  if (data === null) return 'null'
  if (data === Infinity) return 'Infinity'
  if (data === -Infinity) return '(-Infinity)'
  if (data !== data) return 'NaN'
  if (typeof data === 'symbol') return ''
  if (typeof data === 'function') return nativeFuncMap.get(data) ?? `(${data.toString()})`
  if (typeof data === 'bigint') return `(${data.toString()}n)`
  if (typeof data !== 'object') return JSON.stringify(data)
  if (Array.isArray(data)) return `[${data.map(stringifyValue).join(', ')}]`
  if (data instanceof Date) return `(new Date(${data.getTime()}))`
  if (data instanceof Error) {
    const { name, stack, message } = data
    const constructor = globalKeys.includes(data.constructor.name) ? data.constructor.name : 'Error'
    return `(() => {
      const e = {
        name: ${stringifyValue(name)},
        stack: ${stringifyValue(stack)},
        message: ${stringifyValue(message)}
      };
      Object.setPrototypeOf(e, ${constructor});
      return e;
    })()`.replace(/\s+/gs, ' ')
  }
  if (data instanceof RegExp) return `(${data.toString()})`
  if (data instanceof Set) return `(new Set(${stringifyValue([...data])}))`
  if (data instanceof Map) return `(new Map(${stringifyValue([...data])}))`
  if (data instanceof WeakMap) return `(new WeakMap)`
  if (data instanceof WeakSet) return `(new WeakSet)`
  if (data instanceof Buffer) return `(Buffer.from(${stringifyValue([...data])}))`
  if (data instanceof Int8Array) return `(new Int8Array(${stringifyValue([...data])}))`
  if (data instanceof Int16Array) return `(new Int16Array(${stringifyValue([...data])}))`
  if (data instanceof Int32Array) return `(new Int32Array(${stringifyValue([...data])}))`
  if (data instanceof Uint8Array) return `(new Uint8Array(${stringifyValue([...data])}))`
  if (data instanceof Uint8ClampedArray)
    return `(new Uint8ClampedArray(${stringifyValue([...data])}))`
  if (data instanceof Uint16Array) return `(new Uint16Array(${stringifyValue([...data])}))`
  if (data instanceof Uint32Array) return `(new Uint32Array(${stringifyValue([...data])}))`
  if (data instanceof BigInt64Array) return `(new BigInt64Array(${stringifyValue([...data])}))`
  if (data instanceof BigUint64Array) return `(new BigUint64Array(${stringifyValue([...data])}))`
  if (data instanceof Float32Array) return `(new Float32Array(${stringifyValue([...data])}))`
  if (data instanceof Float64Array) return `(new Float64Array(${stringifyValue([...data])}))`
  return `({
    ${Object.entries(data)
      .map(([key, value]) => {
        value = stringifyValue(value)
        if (!value) return ''
        return `${key}:${value}`
      })
      .filter((str) => str)
      .join(',')}
  })`.replace(/\s+/gs, ' ')
}
